package com.lwj.mallchat.common.websocket.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.lwj.mallchat.common.common.event.UserOnlineEvent;
import com.lwj.mallchat.common.user.dao.UserDao;
import com.lwj.mallchat.common.user.domain.entity.User;
import com.lwj.mallchat.common.user.domain.enums.RoleEnum;
import com.lwj.mallchat.common.user.service.LoginService;
import com.lwj.mallchat.common.user.service.RoleService;
import com.lwj.mallchat.common.websocket.NettyUtil;
import com.lwj.mallchat.common.websocket.domain.dto.WSChannelExtraDTO;
import com.lwj.mallchat.common.websocket.domain.vo.resp.WSBaseResp;
import com.lwj.mallchat.common.websocket.service.WebSocketService;
import com.lwj.mallchat.common.websocket.service.adapter.WSAdapter;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @author lwj
 * @ClassName WebSocketServiceImpl
 * @date 2024/10/17 14:39
 */
@Service
@Slf4j
public class WebSocketServiceImpl implements WebSocketService {





    @Autowired
    @Lazy
    private WxMpService wxMpService;

    @Autowired
    private UserDao userDao;

    @Autowired
    private LoginService loginService;

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    private RoleService roleService;

    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    /**
     * 管理所有用户的连接（登录态/游客）
     */
    private static final ConcurrentHashMap<Channel, WSChannelExtraDTO> ONLINE_WS_MAP = new ConcurrentHashMap<>();

    /**
     * 所有在线的用户和对应的socket
     */
    private static final ConcurrentHashMap<Long, CopyOnWriteArrayList<Channel>> ONLINE_UID_MAP = new ConcurrentHashMap<>();

    public static final int MAXIMUM_SIZE = 1000;

    public static final Duration DURATION = Duration.ofHours(1);

    /**
     * 临时保存登录code跟Channel的映射关系
     */
    private static final Cache<Integer, Channel> WAIT_LOGIN_MAP = Caffeine.newBuilder()
            .maximumSize(MAXIMUM_SIZE)
            .expireAfterWrite(DURATION)
            .build();

    @Override
    public void connect(Channel channel) {
        ONLINE_WS_MAP.put(channel, new WSChannelExtraDTO());
    }

    @SneakyThrows
    @Override
    public void handleLogReq(Channel channel) {
        //生成随机码
        Integer code = generateLoginCode(channel);
        //把微信申请带参二维码
        WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, (int) DURATION.getSeconds());
        //把码推送给前端
        sendMsg(channel, WSAdapter.buildResp(wxMpQrCodeTicket));
    }

    @Override
    public void remove(Channel channel) {
        ONLINE_WS_MAP.remove(channel);
        //todo 用户下线逻辑处理
    }

    @Override
    public void scanLogSuccess(Integer eventKey, Long id) {
        /**
         * 确认连接在机器上
         */
        Channel channel = WAIT_LOGIN_MAP.getIfPresent(eventKey);
        if (Objects.isNull(channel)) {
            return;
        }
        User user = userDao.getById(id);
        //移除code
        WAIT_LOGIN_MAP.invalidate(eventKey);
        //调用登录模块获取token
        String token = loginService.login(id);
        //用户登录
        loginSuccess(channel, user, token);
    }

    @Override
    public void waitAuthorize(Integer eventKey) {
        Channel channel = WAIT_LOGIN_MAP.getIfPresent(eventKey);
        if (Objects.isNull(channel)) {
            return;
        }
        sendMsg(channel, WSAdapter.buildWaitAuthorizeResp());
    }

    @Override
    public void authorize(Channel channel, String token) {
        Long uid = loginService.getValidUid(token);
        if (Objects.nonNull(uid)) {
            User user = userDao.getById(uid);
            loginSuccess(channel, user, token);
        } else {
            sendMsg(channel, WSAdapter.buildInvalidTokenResp());
        }
    }

    @Override
    public void sendMsgToAll(WSBaseResp<?> msg) {
        ONLINE_WS_MAP.forEach((channel, ext) -> {
            threadPoolTaskExecutor.execute(()->{
                sendMsg(channel, msg);
            });
        });
    }

    private void loginSuccess(Channel channel, User user, String token) {
        //保存对应channel的对应uid
        WSChannelExtraDTO channelExtraDTO = new WSChannelExtraDTO();
        channelExtraDTO.setUid(user.getId());
        ONLINE_WS_MAP.putIfAbsent(channel, channelExtraDTO);
        //推送登陆成功消息
        sendMsg(channel, WSAdapter.buildResp(user, token, roleService.hasPower(user.getId(), RoleEnum.CHAT_MANAGER)));
        //用户上线成功事件
        user.setLastOptTime(new Date());
        user.refreshIp(NettyUtil.getAttr(channel, NettyUtil.IP));
        applicationEventPublisher.publishEvent(new UserOnlineEvent(this, user));
    }

    private void sendMsg(Channel channel, WSBaseResp<?> resp) {
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(resp)));
    }

    private Integer generateLoginCode(Channel channel) {
        Integer code;
        do {
            code = RandomUtil.randomInt(Integer.MAX_VALUE);
        } while (Objects.nonNull(WAIT_LOGIN_MAP.asMap().putIfAbsent(code, channel)));
        return code;
    }


    @Override
    public void sendToUid(WSBaseResp<?> wsBaseResp, Long uid) {
        CopyOnWriteArrayList<Channel> channels = ONLINE_UID_MAP.get(uid);
        if (CollectionUtil.isEmpty(channels)) {
            log.info("用户：{}不在线", uid);
            return;
        }
        channels.forEach(channel -> {
            threadPoolTaskExecutor.execute(() -> sendMsg(channel, wsBaseResp));
        });
    }

    //entrySet的值不是快照数据,但是它支持遍历，所以无所谓了，不用快照也行。
    @Override
    public void sendToAllOnline(WSBaseResp<?> wsBaseResp, Long skipUid) {
        ONLINE_WS_MAP.forEach((channel, ext) -> {
            if (Objects.nonNull(skipUid) && Objects.equals(ext.getUid(), skipUid)) {
                return;
            }
            threadPoolTaskExecutor.execute(() -> sendMsg(channel, wsBaseResp));
        });
    }


    @Override
    public void sendToAllOnline(WSBaseResp<?> wsBaseResp) {
        sendToAllOnline(wsBaseResp, null);
    }



}
